package com.xdb.sqlparser.util;

import java.util.List;

import com.xdb.sqlparser.ast.SQLDataType;
import com.xdb.sqlparser.ast.SQLExpr;
import com.xdb.sqlparser.ast.SQLObject;
import com.xdb.sqlparser.ast.SQLStatement;
import com.xdb.sqlparser.ast.expr.SQLBinaryOpExpr;
import com.xdb.sqlparser.ast.expr.SQLBinaryOperator;
import com.xdb.sqlparser.ast.expr.SQLCharExpr;
import com.xdb.sqlparser.ast.expr.SQLHexExpr;
import com.xdb.sqlparser.ast.expr.SQLInListExpr;
import com.xdb.sqlparser.ast.expr.SQLIntegerExpr;
import com.xdb.sqlparser.ast.expr.SQLLiteralExpr;
import com.xdb.sqlparser.ast.expr.SQLNCharExpr;
import com.xdb.sqlparser.ast.expr.SQLNullExpr;
import com.xdb.sqlparser.ast.expr.SQLNumberExpr;
import com.xdb.sqlparser.ast.expr.SQLVariantRefExpr;
import com.xdb.sqlparser.ast.statement.SQLColumnDefinition;
import com.xdb.sqlparser.ast.statement.SQLSelectOrderByItem;
import com.xdb.sqlparser.dialect.mysql.visitor.MySqlParameterizedOutputVisitor;
import com.xdb.sqlparser.dialect.oracle.visitor.OracleParameterizedOutputVisitor;
import com.xdb.sqlparser.dialect.sql99.parser.SQL99StatementParser;
import com.xdb.sqlparser.visitor.SQLParameterizedOutputVisitor;
import com.xdb.sqlparser.visitor.SQLParameterizedVisitor;
import com.xdb.util.JdbcUtils;

public class ParameterizedOutputVisitorUtils {

	public static final String ATTR_PARAMS_SKIP = "parameterized.skip";

	public static String parameterize(String sql, String dbType) {
		SQL99StatementParser parser = SQLParserUtils.createSQLStatementParser(sql, dbType);
		List<SQLStatement> statementList = parser.parseStatementList();
		if (statementList.size() == 0) {
			return sql;
		}

		StringBuilder out = new StringBuilder();
		SQLParameterizedVisitor visitor = createParameterizedOutputVisitor(out, dbType);

		for (int i = 0; i < statementList.size(); i++) {
			if (i > 0) {
				out.append(";\n");
			}
			SQLStatement stmt = statementList.get(i);
			stmt.accept(visitor);
		}

		if (visitor.getReplaceCount() == 0 && !parser.getLexer().isHasComment()) {
			return sql;
		}

		return out.toString();
	}

	public static SQLParameterizedVisitor createParameterizedOutputVisitor(Appendable out, String dbType) {
		if (JdbcUtils.ORACLE.equals(dbType)) {
			return new OracleParameterizedOutputVisitor(out);
		}

		if (JdbcUtils.MYSQL.equals(dbType)) {
			return new MySqlParameterizedOutputVisitor(out);
		}

		return new SQLParameterizedOutputVisitor(out);
	}

	public static boolean visit(SQLParameterizedVisitor v, SQLInListExpr x) {
		List<SQLExpr> targetList = x.getTargetList();

		boolean changed = true;
		if (targetList.size() == 1 && targetList.get(0) instanceof SQLVariantRefExpr) {
			changed = false;
		}

		x.getExpr().accept(v);

		if (x.isNot()) {
			v.print(" NOT IN (?)");
		} else {
			v.print(" IN (?)");
		}

		if (changed) {
			v.incrementReplaceCunt();
		}

		return false;
	}

	public static boolean visit(SQLParameterizedVisitor v, SQLIntegerExpr x) {
		if (!checkParameterize(x)) {
			return SQLASTOutputVisitorUtils.visit(v, x);
		}

		v.print('?');
		v.incrementReplaceCunt();
		return false;
	}

	public static boolean visit(SQLParameterizedVisitor v, SQLNumberExpr x) {
		if (!checkParameterize(x)) {
			return SQLASTOutputVisitorUtils.visit(v, x);
		}

		v.print('?');
		v.incrementReplaceCunt();
		return false;
	}

	public static boolean visit(SQLParameterizedVisitor v, SQLCharExpr x) {
		v.print('?');
		v.incrementReplaceCunt();
		return false;
	}

	public static boolean checkParameterize(SQLObject x) {
		if (Boolean.TRUE.equals(x.getAttribute(ParameterizedOutputVisitorUtils.ATTR_PARAMS_SKIP))) {
			return false;
		}

		SQLObject parent = x.getParent();

		if (parent instanceof SQLDataType //
				|| parent instanceof SQLColumnDefinition //
				|| parent instanceof SQLSelectOrderByItem //
		) {
			return false;
		}

		return true;
	}

	public static boolean visit(SQLParameterizedVisitor v, SQLNCharExpr x) {
		v.print('?');
		v.incrementReplaceCunt();
		return false;
	}

	public static boolean visit(SQLParameterizedVisitor v, SQLNullExpr x) {
		SQLObject parent = x.getParent();
		if (parent instanceof SQLBinaryOpExpr) {
			SQLBinaryOpExpr binaryOpExpr = (SQLBinaryOpExpr) parent;
			if (binaryOpExpr.getOperator() == SQLBinaryOperator.IsNot || binaryOpExpr.getOperator() == SQLBinaryOperator.Is) {
				v.print("NULL");
				return false;
			}
		}

		v.print('?');
		v.incrementReplaceCunt();
		return false;
	}

	public static boolean visit(SQLParameterizedVisitor v, SQLVariantRefExpr x) {
		v.print('?');
		v.incrementReplaceCunt();
		return false;
	}

	public static boolean visit(SQLParameterizedVisitor v, SQLHexExpr x) {
		v.print('?');
		v.incrementReplaceCunt();
		return false;
	}

	public static SQLBinaryOpExpr merge(SQLParameterizedVisitor v, SQLBinaryOpExpr x) {
		SQLExpr left = x.getLeft();
		SQLExpr right = x.getRight();
		SQLObject parent = x.getParent();

		if (left instanceof SQLLiteralExpr && right instanceof SQLLiteralExpr) {
			if (x.getOperator() == SQLBinaryOperator.Equality //
					|| x.getOperator() == SQLBinaryOperator.NotEqual) {
				if ((left instanceof SQLIntegerExpr) && (right instanceof SQLIntegerExpr)) {
					if (((SQLIntegerExpr) left).getNumber().intValue() < 100) {
						left.putAttribute(ATTR_PARAMS_SKIP, true);
					}
					if (((SQLIntegerExpr) right).getNumber().intValue() < 100) {
						right.putAttribute(ATTR_PARAMS_SKIP, true);
					}
				} else {
					left.putAttribute(ATTR_PARAMS_SKIP, true);
					right.putAttribute(ATTR_PARAMS_SKIP, true);
				}
			}
			return x;
		}

		for (;;) {
			if (x.getRight() instanceof SQLBinaryOpExpr) {
				if (x.getLeft() instanceof SQLBinaryOpExpr) {
					SQLBinaryOpExpr leftBinaryExpr = (SQLBinaryOpExpr) x.getLeft();
					if (leftBinaryExpr.getRight().equals(x.getRight())) {
						x = leftBinaryExpr;
						v.incrementReplaceCunt();
						continue;
					}
				}
				SQLExpr mergedRight = merge(v, (SQLBinaryOpExpr) x.getRight());
				if (mergedRight != x.getRight()) {
					x = new SQLBinaryOpExpr(x.getLeft(), x.getOperator(), mergedRight);
					v.incrementReplaceCunt();
				}
				x.setParent(parent);
			}

			break;
		}

		if (x.getLeft() instanceof SQLBinaryOpExpr) {
			SQLExpr mergedLeft = merge(v, (SQLBinaryOpExpr) x.getLeft());
			if (mergedLeft != x.getLeft()) {
				x = new SQLBinaryOpExpr(mergedLeft, x.getOperator(), x.getRight());
				v.incrementReplaceCunt();
			}
			x.setParent(parent);
		}

		// ID = ? OR ID = ? => ID = ?
		if (x.getOperator() == SQLBinaryOperator.BooleanOr) {
			if ((left instanceof SQLBinaryOpExpr) && (right instanceof SQLBinaryOpExpr)) {
				SQLBinaryOpExpr leftBinary = (SQLBinaryOpExpr) x.getLeft();
				SQLBinaryOpExpr rightBinary = (SQLBinaryOpExpr) x.getRight();

				if (mergeEqual(leftBinary, rightBinary)) {
					v.incrementReplaceCunt();
					return leftBinary;
				}

				if (isLiteralExpr(leftBinary.getLeft()) //
						&& leftBinary.getOperator() == SQLBinaryOperator.BooleanOr) {
					if (mergeEqual(leftBinary.getRight(), right)) {
						v.incrementReplaceCunt();
						return leftBinary;
					}
				}
			}
		}

		return x;
	}

	private static boolean mergeEqual(SQLExpr a, SQLExpr b) {
		if (!(a instanceof SQLBinaryOpExpr)) {
			return false;
		}
		if (!(b instanceof SQLBinaryOpExpr)) {
			return false;
		}

		SQLBinaryOpExpr binaryA = (SQLBinaryOpExpr) a;
		SQLBinaryOpExpr binaryB = (SQLBinaryOpExpr) b;

		if (binaryA.getOperator() != SQLBinaryOperator.Equality) {
			return false;
		}

		if (binaryB.getOperator() != SQLBinaryOperator.Equality) {
			return false;
		}

		if (!(binaryA.getRight() instanceof SQLLiteralExpr || binaryA.getRight() instanceof SQLVariantRefExpr)) {
			return false;
		}

		if (!(binaryB.getRight() instanceof SQLLiteralExpr || binaryB.getRight() instanceof SQLVariantRefExpr)) {
			return false;
		}

		return binaryA.getLeft().toString().equals(binaryB.getLeft().toString());
	}

	private static boolean isLiteralExpr(SQLExpr expr) {
		if (expr instanceof SQLLiteralExpr) {
			return true;
		}

		if (expr instanceof SQLBinaryOpExpr) {
			SQLBinaryOpExpr binary = (SQLBinaryOpExpr) expr;
			return isLiteralExpr(binary.getLeft()) && isLiteralExpr(binary.getRight());
		}

		return false;
	}
}
